package com.ruben.jwtdemo.service.impl;

import com.alibaba.fastjson.JSON;
import com.auth0.jwt.JWT;
import com.auth0.jwt.JWTVerifier;
import com.auth0.jwt.algorithms.Algorithm;
import com.auth0.jwt.exceptions.JWTDecodeException;
import com.auth0.jwt.exceptions.JWTVerificationException;
import com.auth0.jwt.interfaces.DecodedJWT;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.baomidou.mybatisplus.extension.api.R;
import com.ruben.jwtdemo.constant.UserConstant;
import com.ruben.jwtdemo.mapper.UserMapper;
import com.ruben.jwtdemo.pojo.User;
import com.ruben.jwtdemo.service.UserService;
import com.ruben.jwtdemo.utils.Encrypt;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Lazy;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Isolation;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.StringUtils;

import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
import java.io.UnsupportedEncodingException;
import java.net.URLDecoder;
import java.util.Arrays;
import java.util.Date;
import java.util.HashMap;
import java.util.Optional;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicReference;

/**
 * 用户业务层实现类
 *
 * @author <achao1441470436@gmail.com>
 * @since 2021/6/6 0006 22:13
 */
@Slf4j
@Service
public class UserServiceImpl implements UserService {

    @Resource
    private StringRedisTemplate stringRedisTemplate;
    @Lazy
    @Resource
    private UserService userService;
    @Resource
    private UserMapper userMapper;


    @Override
    public String getToken(HttpServletRequest request) {
        // 第一步，从参数获取token
        AtomicReference<String> token = new AtomicReference<>(Optional.ofNullable(request.getParameter("token"))
                // 如果没获取到，我们从header获取
                .orElse(request.getHeader("token")));
        if (StringUtils.isEmpty(token.get())) {
            // 如果还是没获取到，我们从cookie获取
            Optional.ofNullable(request.getCookies())
                    // 遍历cookie
                    .flatMap(cookies -> Arrays.stream(cookies)
                            // 找到名为token的Cookie对象
                            .filter(c -> "token".equals(c.getName()))
                            .findAny()).ifPresent(c -> {
                try {
                    // 如果存在，则使用URLDecoder解码，并返回
                    token.set(URLDecoder.decode(c.getValue(), "utf-8"));
                } catch (UnsupportedEncodingException e) {
                    log.error("cookie转换错误", e);
                }
            });
        }
        return token.get();
    }

    @Override
    public User verifyToken(String refreshToken) {
        User user;
        try {
            // 从缓存中获取key为"token_"+token的value
            String token = stringRedisTemplate.opsForValue().get(UserConstant.TOKEN + refreshToken);
            if (StringUtils.isEmpty(token)) {
                // 如果没获取到，则返回null
                return null;
            }
            if (!token.equals(refreshToken)) {
                // 如果获取到和传入的token不相同，则返回null
                // 传入的token应该是不带"token_"开头的，就是普通生成的token
                // 但我们缓存中，key是"token_"开头，再加上我们token，这样去存储的key
                return null;
            }
            // 从token中获取唯一用户名，这个是用户用于登陆的，具有唯一性，需要在数据库中加唯一索引
            String username = userService.getUsernameByToken(refreshToken);
            // 根据用户名查询用户
            user = getUserByUsername(username);
            // 这里通过密码，生成并指定Algorithm中的HMAC256算法，用于校验token
            Algorithm algorithm = Algorithm.HMAC256(user.getPassword());
            // 通过上方的algorithm算法创建校验器
            JWTVerifier verifier = JWT.require(algorithm)
                    // 指定用户名作为特定的存取凭证
                    .withClaim("username", username).build();
            // 执行校验
            verifier.verify(refreshToken);
        } catch (IllegalArgumentException | JWTVerificationException e) {
            // 校验失败直接return null
            return null;
        }
        // 校验成功后，返回用户信息
        return user;
    }

    @Override
    public String getUsernameByToken(String token) {
        try {
            // 通过JWT工具包解密token，然后获取用户名并返回
            DecodedJWT jwt = JWT.decode(token);
            return jwt.getClaim("username").asString();
        } catch (JWTDecodeException e) {
            return null;
        }
    }

    @Override
    @Transactional(isolation = Isolation.SERIALIZABLE)
    public User getUserByUsername(String username) {
        if (StringUtils.isEmpty(username)) {
            return null;
        }
        //从缓存中获取，获取不到再从数据库中获取
        String userJson = (String) stringRedisTemplate
                .opsForHash()
                .get(UserConstant.USER_CACHE,
                        UserConstant.LOGIN_USER_PRE + username);
        if (StringUtils.isEmpty(userJson)) {
            //从数据库中获取
            return userMapper.selectOne(Wrappers.lambdaQuery(User.builder().username(username).build()));
        }
        return JSON.parseObject(userJson, User.class);
    }

    @Override
    public User validateUser(User user) {
        String username = user.getUsername();
        // 通过用户名查询用户
        User userFromServer = getUserByUsername(username);
        // 如果用户没查到，直接返回null
        if (userFromServer == null) {
            return null;
        }
        //存入缓存
        stringRedisTemplate
                .opsForHash()
                .put(UserConstant.USER_CACHE,
                        // 注意这里key加了前缀
                        UserConstant.LOGIN_USER_PRE + user.getUsername(),
                        // 用户信息转换成json存redis
                        JSON.toJSONString(userFromServer));
        // 密码校验(对明文密码进行加密，然后比对数据库中加密的密码，这里加密规则为SALT+用户名+密码的字符串再进行SHA512加密)
        String password = Encrypt.SHA512(UserConstant.SALT + user.getUsername() + user.getPassword());
        if (!password.equals(userFromServer.getPassword())) {
            // 如果加密后的密码，和数据库中不一致，则密码错误
            return null;
        }
        return userFromServer;
    }

    @Override
    public String createToken(User user, int expire) {
        // 通过jwt工具类
        String token = JWT.create()
                // 指定用户名作为特定的存取凭证
                .withClaim("username", user.getUsername())
                // 设置过期时间
                .withExpiresAt(new Date(System.currentTimeMillis() + expire))
                // 这里通过密码，生成并指定Algorithm中的HMAC256算法，用于校验token
                .sign(Algorithm.HMAC256(user.getPassword()));
        // 然后把token存redis，设置过期时间
        stringRedisTemplate.opsForValue().set(UserConstant.TOKEN + token, token, expire, TimeUnit.MILLISECONDS);
        return token;
    }

    /**
     * `token`过期后返回"`token`过期对应的`code`"，客户端使用一个大于`token`过期时间的`refreshToken`去调用刷新`token`的接口，
     * `refreshToken`通过校验之后，直接生成新的`token`
     * 我这里设置的两倍，这样在超过token有效期一倍，小于两倍时，期间可以刷新token，再超时就需要重新登录了
     *
     * @param refreshToken
     * @return
     */
    @Override
    public R refreshToken(String refreshToken) {
        try {
            // 校验refreshToken，逻辑和token一样
            User user = verifyToken(refreshToken);
            if (user == null) {
                // 校验失败则返回
                return R.failed("失效的token");
            }
            // 校验成功，我们生成新的token并返回
            String token = createToken(user, UserConstant.TOKEN_EXPIRE_TIME);
            // 解密refreshToken，获取过期时间毫秒数
            long expireTime = JWT.decode(refreshToken).getExpiresAt().getTime();
            if (expireTime - System.currentTimeMillis() <= UserConstant.TOKEN_REFRESH_TIME) {
                // 如果token过期时间减去当前时间毫秒数小于等于TOKEN_REFRESH_TIME
                // 创建新的token
                refreshToken = createToken(user, UserConstant.REFRESH_TOKEN_EXPIRE_TIME);
            }
            // 返回token
            HashMap<Object, Object> data = new HashMap<>(4);
            data.put("token", token);
            data.put("refreshToken", refreshToken);
            return R.ok(data);
        } catch (Exception e) {
            log.error("失效的token", e);
            return R.failed("失效的token");
        }
    }


    @Override
    public boolean logout(String token) {
        try {
            String username = getUsernameByToken(token);
            // 清除缓存
            stringRedisTemplate
                    .opsForHash()
                    .delete(UserConstant.USER_CACHE,
                            UserConstant.LOGIN_USER_PRE + username);
            stringRedisTemplate
                    .opsForHash()
                    .delete(UserConstant.TOKEN + token);
        } catch (Exception e) {
            log.error("注销失败，记录日志", e);
            return false;
        }
        return true;
    }

    /**
     * 注册用户
     *
     * @param param
     * @return
     */
    @Override
    @Transactional(rollbackFor = Exception.class)
    public String register(User param) {
        // 密码加密
        String password = Encrypt.SHA512(UserConstant.SALT + param.getUsername() + param.getPassword());
        User user = User.builder()
                .id(new Long(System.currentTimeMillis()).intValue())
                .username(param.getUsername())
                .password(password)
                .build();
        try {
            // 插入用户
            int affect = userMapper.insert(user);
            if (affect != 1) {
                return "添加用户失败";
            }
        } catch (Exception e) {
            e.printStackTrace();
            return "添加用户失败";
        }
        return null;
    }
}
